NestJS + TypeORM 进行 Migration

您所在的位置:网站首页 typeorm 迁移 NestJS + TypeORM 进行 Migration

NestJS + TypeORM 进行 Migration

2024-06-04 08:54| 来源: 网络整理| 查看: 265

什么是 Migration

当我们使用 ORM 框架的时候,例如 TypeORM,我们会建立 JavaScript 类型到关系型数据库 schema 的映射。当我们建立/修改类型的时候,数据库的表也会有相应的改变。当我们运行我们的程序的时候,我们的 JS 侧会有一个数据的模型。当 JS 侧的模型和 DB 的 schema 符合的时候,我们的程序才能正常运行。所谓 Migration,也就是当 JS 侧的模型改变了,我们要运行怎样的程序才能让 DB 也进行同步的改变。

NestJS 的 Migration 有什么问题?

在 NestJS 的文档中,明确写到 “Migration 由 TypeORM 提供的 CLI 来管理”。NestJS 并不为数据库的迁移提供任何的 API。这个时候,我们可能需要把 TypeORM 的 orm.config.js/orm.config.json 从服务器的代码中提取出来,然后根据共享的这份 config 来分别运行 TypeORM CLI 和我们的 NestJS 服务器。

typeorm migration:create -n PostRefactoring

这就会出现一个问题:假如我们的 TypeORM 中的模型依赖于运行时的数据,而这个数据无法被 CLI 所直接获得,该怎么办呢?假如我们的 entity 是通过 autoLoadEntities 来加载的,这些信息将无法被 TypeORM 的 config 直接获取,因为它依赖 NestJS 的依赖注入等运行时。

让 Server 自己解决 Migration

假如分析 Server 的进程,实际上它分成两部分:

构建 AppModule。这一步包含了构建所有的依赖,自然也包括 TypeORM 的模型构建。进入等待请求,处理请求的无限循环。function bootstrap() { const app = await NestFactory.create(AppModule); // Step 1 await app.listen(port); // Step 2 } bootstrap();

那么答案呼之欲出,如果我们能够为 nest start 提供环境变量 —— GEN_MIGRATION —— 当然,你也可以用命令行参数而不使用环境变量,然后在 Step 1 后判断是否该选项成立,是的话则运行"生成 migration 程序"的操作。因为此时,我们已经拿到了所有 TypeORM 模型的信息,所以理论上我们可以在这里进行 migration 的操作。

import { Connection } from 'typeorm'; function bootstrap() { const app = await NestFactory.create(AppModule); // Step 1 if (process.env.GEN_MIGRATION) { const connection = app.get(Connection); // 拿到 TypeORM 的 connection await generateMigration(connection, process.env.GEN_MIGRATION); return; } await app.listen(port); // Step 2 } bootstrap();

那么 generateMigration 该怎么写呢?很可惜,TypeORM 没有提供生成 migration 文件的 API。它的生成过程是硬编码在 CLI 中的。也就是说,目前除了 CLI 以外没有别的 migration 生成办法……

山重水复疑无路,有时候还是方法比困难多的。既然 CLI 不提供,那我们看看 CLI 做了什么,然后照着做一遍就行嘛。阅读 TypeORM 命令行的代码 [MigrationGenerateCommand]((https://github.com/typeorm/typeorm/blob/master/src/commands/MigrationGenerateCommand.ts),我们可以发现,关键的调用在这里:

const sqlInMemory = await connection.driver.createSchemaBuilder().log();

其中 connection.driver 是 TypeORM 的数据库驱动,也就是客户端。它是一个界面,具体实现可以是 MysqlDriver, PostgresDriver 等等,和具体的数据库一一对应。查询 createSchemaBuilder,它不在官方文档中,但在 API Reference 中出现了。它的作用我们也不难猜出就是生成迁移数据库的 SQL 指令。而我们需要把这些指令,合适地放入 Migration 文件中,就可以让 TypeORM 像执行它自己生成的 migration 一样执行我们的 migration 了。

具体的 make migration / migrate 代码附于下方,欢迎参考。

// utils/generateMigration.ts // Adapted from https://github.com/typeorm/typeorm/blob/master/src/commands/MigrationGenerateCommand.ts import { Logger } from '@nestjs/common'; import { Connection } from 'typeorm'; import * as prettier from 'prettier'; import { Query } from 'typeorm/driver/Query'; import * as fs from 'fs/promises'; import dayjs from 'dayjs'; const getTemplate = ( name: string, timestamp: number, upSqls: string[], downSqls: string[], ): string => { const migrationName = `${name}${timestamp}`; return ` const {MigrationInterface, QueryRunner} = require('typeorm'); class ${migrationName} { name = '${migrationName}' async up(queryRunner) { ${upSqls.join('\n')} } async down(queryRunner) { ${downSqls.join('\n')} } } module.exports = ${migrationName}; `; }; const queryParams = (parameters: any[] | undefined): string => { if (!parameters || !parameters.length) { return ''; } return `, ${JSON.stringify(parameters)}`; }; export const generateMigration = async ( connection: Connection, name: string, ) => { const queries = await connection.driver.createSchemaBuilder().log(); if (queries.upQueries.length === 0) { Logger.warn('Database is up-to-date. '); return; } const mapSqlToJs = (query: Query) => { return ( 'await queryRunner.query(`' + query.query.replace(new RegExp('`', 'g'), '\\`') + '`' + queryParams(query.parameters) + ');' ); }; const upSqls: string[] = queries.upQueries.map(mapSqlToJs), downSqls: string[] = queries.downQueries.map(mapSqlToJs); const code = getTemplate(name, new Date().getTime(), upSqls, downSqls); const config = await prettier.resolveConfig(process.cwd()); const formatted = prettier.format(code, { ...config, parser: 'babel-ts', }); try { await fs.readdir('migrations'); } catch (_) { await fs.mkdir('migrations', { recursive: true, }); } const fileName = `migrations/${dayjs().format('YYYYMMDDHHmm')}-${name}.js`; await fs.writeFile(fileName, formatted); Logger.log(`Success! Migration file created at ${fileName}`); }; // main.ts import { NestFactory } from '@nestjs/core'; import { Config } from 'config/config'; import { Connection } from 'typeorm'; import { AppModule } from './app.module'; import { generateMigration } from './utils/generateMigration'; async function bootstrap() { const app = await NestFactory.create(AppModule); if (process.env.GEN_MIGRATION) { const connection = app.get(Connection); await generateMigration(connection, process.env.GEN_MIGRATION); return; } if (process.env.MIGRATE) { const connection = app.get(Connection); Logger.log(`process.env.MIGRATE is set, migrating...`); const migrations = await connection.runMigrations(); if (migrations.length === 0) { Logger.warn(`Your database is up-to-date, no migrations were applied. `); } else { Logger.log(`${migrations.length} complete.`); } return; } await app.listen(3000); } bootstrap(); 总结

NestJS 不提供 migration 的 API,TypeORM 的 migration 目前官方只支持命令行,而不支持程序化调用,给配置带来了麻烦。本文提供了一个未见于官网文档的 API 的使用方法,方便读者参考。当然,最好的办法应该是 TypeORM 提供 migration 的程序接口,并且将 NestJS 和 TypeORM 封装成一体的框架,以免重复解决相同的问题。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3